#include "process_management.h"

/* global variables */

/* this array contains all the PCBs. Until we have a way to allocate memory dynamically, its size is fixed and each PCB has a flag in_use indicating whether it is being used or not */
static PCB processes[NUMBER_MAX_PROCESSES];

/* counts the number of processes in the system */
static int32_t count_processes;

/* this array represents the stacks for the processes. */
static uint32_t stacks[NUMBER_MAX_PROCESSES*STACK_SIZE]; 

/* function bodies */

void initialize_processes_table(){
  int i;
  count_processes = 0;
  for(i=0;i<NUMBER_MAX_PROCESSES;i++){
    processes[i].in_use = 0; //we mark this PCB as being unused
    /* reset supervisors and supervised pid to standard value */
    supervision_queue_initialize(&(processes[i].supervisors));
    supervision_queue_initialize(&(processes[i].supervised));
  }
}

int32_t create_process(uint32_t priority, char* program_name, int32_t parent,int32_t parameters[4]){
  int32_t j,i, program_id;
  uint32_t entry_point;
  PCB* pcb;
  registers_t* regs;

  if(priority >= MAX_PRIORITY)
    //this priority is not possible in this system
    return -1;

  if(count_processes >= NUMBER_MAX_PROCESSES)
    return -1; //the system can't accept a new process anymore
  
  for(i=0;i<NUMBER_MAX_PROCESSES;i++){
    pcb = &processes[i];
    if(!pcb_get_in_use(pcb)){
      pcb_set_in_use(pcb, 1); //this PCB is now used by a process
      pcb_set_pid(pcb, i); /* the pid of the process is its position in the array */
      /* let's get the entry point of the program */
      program_id = get_program_id(program_name);
      if(program_id < 0)
        return -1; 
      else {
        entry_point = get_entry_point(program_id);
        if(entry_point == 0)
          return -1;
      }
      /* we register the information got above */
      pcb_set_program_id(pcb, program_id);
      pcb_set_epc(pcb, entry_point); 
      pcb_set_priority(pcb, priority);
      pcb_set_state(pcb, READY_STATE);
      pcb_set_parent(pcb, parent);
      // now, we have to allocate a stack. We have reserved enough space for this at compile time.
      pcb_set_sp(pcb, (uint32_t)(&stacks[(i+1)*STACK_SIZE-1])); /*the stack grows towards the lower addresses, that's why we have to use i+1 */
      /* "injects" the parameters directly in the a registers. Either NULL or an array of four integers must be passed. */
      if(parameters != NULL){
        regs = pcb_get_registers(pcb);
        for(j = 0; j < 4; j ++)
          regs->a_reg[j] = parameters[j];
      }
      insert_ready_queue(pcb);
      //one more process
      count_processes ++;
      break; 
    }
  }
  return (i<NUMBER_MAX_PROCESSES?i:-1); //returns -1 if the process couldn't be created
}

int32_t remove_process(int32_t pid, int32_t return_value){
  PCB* pcb;
  pcb = get_process_by_pid(pid);
  if(pcb != NULL) {
    /* call process supervision to notify and delete */
    return remove_process_pcb(pcb,return_value);
  }
  return 0;
}

int32_t remove_process_pcb(PCB* pcb, int32_t return_value){
  uint32_t state = pcb_get_state(pcb);
  /* signal termination and clean up supervisors and supervisees */
  signal_termination(pcb, return_value);
  //removes the PCB from the different places it may be
  switch(state){
    case ZOMBIE_STATE:
      //the process has already be terminated but stayed in the system as a zombie, nothing more can be done
      return 1;
    case RUNNING_STATE:
    case READY_STATE:
      if(!remove_ready_queue(pcb)) //removes this PCB from the ready queue
        return 0;
      break;
    case WAITING_IO_UART_STATE:
      if(!remove_uart_queue(pcb))
        return 0;
      break;
    case DOING_IO_UART_STATE:
      if(!end_of_io() || !remove_ready_queue(pcb))
        return 0;
      break;
    case DELAYED_STATE:
      if(!remove_delayed(pcb))
        return 0;
      break;
    default:
      ; //blocked or waiting for child termination, nothing special to do
  }

  /* removes any allocated memory the process did not free */
  free_all_pid(pcb_get_pid(pcb));
  
  /*
     now, the PCB has been removed from everywhere except the process table.
     We check if it has some children that needs to be adopted by the system
     */
  adopt_children(pcb);

  /*
     Now, let's check if its parent is waiting for the return value. Otherwise, the process will become a zombie. This function also deletes pcb if possible.
     */
  return pass_return_value(pcb,return_value);
}

/* looks for pcb's children and changes their parents to SYSTEM_ID */
void adopt_children(PCB* pcb){
  int32_t i, j = 0;
  PCB* p;
  for(i=0;i<NUMBER_MAX_PROCESSES && j < count_processes;i++){
    p = &processes[i];
    if(pcb_get_in_use(p) && pcb_get_parent(p) == pcb_get_pid(pcb)){
      //we found a child
      j ++;
      if(pcb_get_state(p) == ZOMBIE_STATE)
	/* OMG ! A zombie ! Run !
	   Now that its parent is going to quit, there is no need for this zombie anymore */
	delete_pcb(p); //one bullet, straight in the head...
      else 
	pcb_set_parent(p,SYSTEM_ID);
    }
  }
}

/* marks pcb as unused and decrements count_processes */
void delete_pcb(PCB* pcb){
  pcb_set_in_use(pcb,0);
  count_processes --;
}

/* passes return_value of a process to pcb's parent if it is waiting for it. Otherwise, pcb becomes a zombie */
int32_t pass_return_value(PCB* pcb, int32_t return_value){
  int32_t parent = pcb_get_parent(pcb);
  PCB* parent_pcb;
  if(parent != SYSTEM_ID){
    //the parent of this process is another process
    parent_pcb = get_process_by_pid(parent);
    if(parent_pcb == NULL)
      //this should not be possible
      return 0;
    registers_t* parent_regs = pcb_get_registers(parent_pcb);
    if(pcb_get_state(parent_pcb) == WAITING_CHILD_TERM_STATE && pcb_get_waiting_for(parent_pcb) == pcb_get_pid(pcb)){
      /* the parent was waiting for the termination of this child
         the return_value parameter has to be passed using the registers of the parent */
      *((int32_t*)parent_regs->a_reg[1]) = return_value;
      wake_up_pcb(parent_pcb); //the parent can now resume
    }else{
      //the process becomes a zombie
      pcb_set_state(pcb, ZOMBIE_STATE);
      //saves the return value
      pcb_set_status(pcb, return_value);
      return 1; //we can't deallocate the PCB now
    }
  }
  delete_pcb(pcb); //this PCB is no longer used, that does the pid recycling as well 
  return 1;
}

PCB* get_process_by_pid(int32_t pid){
  PCB* pcb = NULL;
  if(pid >= NUMBER_MAX_PROCESSES || pid < 0)
    //pid not possible 
    return NULL;
  pcb = &processes[pid];
  return (pcb_get_in_use(pcb)?pcb:NULL);
}

/* changes the priority of the process to priority */
int32_t change_priority(int32_t pid, int32_t priority) {
  PCB* target = get_process_by_pid(pid);
  if(target != NULL){
    int32_t old = pcb_get_priority(target);
    pcb_set_priority(target, priority);
    do_change_priority(old, priority);
    return 1;
  }
  return 0;
}

/* blocks the process pid. It is only possible to block a process ready or running (blocking a delayed process would not make much sense) */
int32_t block(int32_t pid){
  PCB* target = get_process_by_pid(pid);
  if(target != NULL)
    return block_pcb(target);
  return 0;
}

/* blocks the process pointed by pcb. It is only possible to block a process ready or running (blocking a delayed process would not make much sense) */
int32_t block_pcb(PCB* target){
  if(pcb_get_state(target) == RUNNING_STATE || 
      pcb_get_state(target) == READY_STATE){
    //the process is not already blocked
    pcb_set_state(target, BLOCKED_STATE);
    if(!remove_ready_queue(target))
      return 0; //failure
    //in case the running process blocked itself, we call the scheduler
    schedule();
    return 1;
  }
  return 0;
}

/* wakes the process pid up */
int32_t wake_up(int32_t pid){
  PCB* target = get_process_by_pid(pid);
  if(target != NULL)
     return wake_up_pcb(target);
  return 0;
}

/* wakes the process pointed by pcb up.
   Returns 1 in case of success, 0 otherwise.
*/
int32_t wake_up_pcb(PCB* target){ 
  uint32_t state = pcb_get_state(target);
  if(!(state  == READY_STATE || state == RUNNING_STATE || state == ZOMBIE_STATE)){
    if(state == DELAYED_STATE)
      //we need to remove the process from the delayed array
      if(!remove_delayed(target))
	return 0;
    pcb_set_state(target,READY_STATE);
    return insert_ready_queue(target);
  }
  return 0;
}


/* delays the process pid during n ms.
   Returns 1 if the process was sucessfully delayed, 0 otherwise.
*/
int32_t delay(int32_t pid, uint32_t n){
  PCB* target = get_process_by_pid(pid);
  if(target != NULL)
    return delay_pcb(target, n);
  return 0;
}

/* delays the process pointed by target during n ms.
   Returns 1 if the process was sucessfully delayed, 0 otherwise. */
int32_t delay_pcb(PCB* target, uint32_t n){
  if(pcb_get_state(target) == RUNNING_STATE || 
     pcb_get_state(target) == READY_STATE){
    pcb_set_state(target,DELAYED_STATE);
    pcb_set_delay(target,n);
    if(!insert_delayed(target))
      //we couldn't insert the pcb in the delayed array, we cannot continue
      return 0;
    if(!remove_ready_queue(target))
      return 0;
    //in case the running process delayed itself, we call the scheduler
    schedule();
    //success !
    return 1;
  }
  return 0;
}

/* Returns the priority of the process pid or -1 if it doesn't exist */
int32_t get_priority(int32_t pid){
    PCB* pcb = get_process_by_pid(pid);
    if(pcb == NULL)
      return -1;
    return pcb_get_priority(pcb);
}

/* Copy the program name of the program executed by pid in program_name (up to n characters). Returns 1 in case of success, 0 otherwise. */
int32_t get_program(int32_t pid, char* program_name, int32_t n){
  PCB* pcb = get_process_by_pid(pid);
  if(pcb == NULL)
    return 0;
  return get_program_name(pcb_get_program_id(pcb),program_name,n);
}

/* returns the state of the process pid or -1 in case of failure */
int32_t get_state(int32_t pid){
  PCB* pcb = get_process_by_pid(pid);
  if(pcb == NULL)
    return -1;
  return pcb_get_state(pcb);
}

/* returns the parent's pid of pid or -2 in case of failure */
int32_t get_parent(int32_t pid){
  PCB* pcb = get_process_by_pid(pid);
  if(pcb == NULL)
    return -2;
  return pcb_get_parent(pcb);
}

/* checks if the process pid has terminated. If it has, its return value is copied in status and the process is deleted. Otherwise, the caller is blocked until such an event occurs. Returns 1 if no error happened (being blocked is NOT an error) or 0 otherwise. */
int32_t wait(int32_t pid, int32_t* status){
  PCB* pcb = get_process_by_pid(pid);
  PCB* caller = get_running();
  if(pcb == NULL)
    //no such process in the system !
    return 0;
  if(pcb_get_parent(pcb) != pcb_get_pid(caller))
    //the pid is not that of a caller's child
    return 0;
  
  //has the child terminated yet? If it has, it's a zombie now 
  if(pcb_get_state(pcb) == ZOMBIE_STATE){
    //the return value is stored in its pcb
    *status = pcb_get_status(pcb);
    //the process can be deleted now
    delete_pcb(pcb); //rest in peace
  }else{
    //the child is not done yet, we have to wait
    block_pcb(caller);
    pcb_set_state(caller, WAITING_CHILD_TERM_STATE);
    pcb_set_waiting_for(caller, pid);
  }
  //We are sure everything is alright. The call cannot fail anymore. So, we can do the return, even if the caller has been blocked
  return 1;
}

					
